package mylittleinvestigations;

import mylittleinvestigations.GameUi.PartnerInformation;
import mylittleinvestigations.CaseInformation.CaseXmlWriter;
import mylittleinvestigations.CaseInformation.Case;
import mylittleinvestigations.CaseInformation.CaseXmlReader;
import mylittleinvestigations.CaseInformation.CaseParseException;
import mylittleinvestigations.GameUi.Tab;
import mylittleinvestigations.GameUi.TabRow;
import java.io.IOException;
import mylittleinvestigations.FieldCharacters.FieldCharacter;
import java.util.*;
import mylittleinvestigations.Collisions.CollisionParameter;
import mylittleinvestigations.Collisions.HitBox;
import mylittleinvestigations.Collisions.OverlapEntry;
import mylittleinvestigations.EasingFunctions.*;
import mylittleinvestigations.Events.LocationEventProvider;
import mylittleinvestigations.FieldCharacters.CharacterDirection;
import mylittleinvestigations.FieldCharacters.FieldCharacterDirection;
import mylittleinvestigations.FieldCharacters.FieldCharacterState;
import mylittleinvestigations.HeightMaps.HeightMap;
import org.newdawn.slick.*;
import org.newdawn.slick.geom.Rectangle;

public class Location
{
    private static final int MinDistanceToRevealHiddenElements = 200;
    private static final int MaxDistanceToStartInteractions = 150;
    private static final int AnimationDuration = 200;

    private Rectangle bounds;
    private Sprite backgroundSprite;
    private FieldCharacter playerCharacter;
    private String previousPartnerCharacterId;
    private FieldCharacter partnerCharacter;
    private ArrayList<FieldCharacter> characterList;
    private ArrayList<Crowd> crowdList;
    private ArrayList<ForegroundElement> foregroundElementList;
    private ArrayList<HiddenForegroundElement> hiddenForegroundElementList;
    private ArrayList<ZoomedView> zoomedViewList;
    private ArrayList<Transition> transitionList;
    private ArrayList<LoopingSound> loopingSoundList;
    private ArrayList<FieldCutscene> cutsceneList;
    private ArrayList<HeightMap> heightMapList;

    private StartPosition startPositionFromMap;
    private HashMap<String, StartPosition> previousLocationIdToStartPositionDictionary;
    private HashMap<String, StartPosition> previousLocationIdToPartnerStartPositionDictionary;

    private Encounter currentEncounter;
    private ForegroundElement currentInteractiveForegroundElement;
    private FieldCutscene currentCutscene;

    private Tab evidenceTab;
    private EvidenceSelector evidenceSelector;

    //private Tab partnerTab;
    private PartnerInformation partnerInformation;
    private int animationOffsetPartner;
    private EasingFunction inEasePartner;
    private EasingFunction outEasePartner;
    private Tab okTab;
    
    private Tab quitTab;

    private static final float BackgroundScale = 2.0f / 3;
    private static final float ForegroundScale = 2.5f;
    private static final int TwilightSpeed = 300;
    
    private InteractiveElement targetInteractiveElement;
    private boolean isTwilightWalking;
    private Queue<Vector2> targetPositionQueue;
    private Vector2 targetPosition;
    private PerformPathfindingThread performPathfindingThread;
    private boolean movingDirectly;
    private final Object pathfindingValuesSyncObject = new Object();
    
    private Vector2 drawingOffsetVector;

    public Location()
    {
        this.foregroundElementList = new ArrayList<ForegroundElement>();
        this.hiddenForegroundElementList = new ArrayList<HiddenForegroundElement>();
        this.zoomedViewList = new ArrayList<ZoomedView>();
        this.characterList = new ArrayList<FieldCharacter>();
        this.crowdList = new ArrayList<Crowd>();
        this.transitionList = new ArrayList<Transition>();
        this.loopingSoundList = new ArrayList<LoopingSound>();
        this.previousLocationIdToStartPositionDictionary = new HashMap<String, StartPosition>();
        this.previousLocationIdToPartnerStartPositionDictionary = new HashMap<String, StartPosition>();
        this.cutsceneIdList = new ArrayList<String>();
        this.heightMapList = new ArrayList<HeightMap>();
    }
    
    private String id;
    
    public String getId() { return this.id; }
    public void setId(String id) { this.id = id; }
    
    private String backgroundTextureId;
    
    public String getBackgroundTextureId() { return this.backgroundTextureId; }
    public void setBackgroundTextureId(String backgroundTextureId) { this.backgroundTextureId = backgroundTextureId; }
    
    private String bgm;

    public String getBgm() { return this.bgm; }
    public void setBgm(String bgm) { this.bgm = bgm; }
    
    private String ambianceSfxId;

    public String getAmbianceSfxId() { return this.ambianceSfxId; }
    public void setAmbianceSfxId(String ambianceSfxId) { this.ambianceSfxId = ambianceSfxId; }

    private Sprite getBackgroundSprite()
    {
        if (this.backgroundSprite == null)
        {
            this.backgroundSprite = Case.getInstance().getSpriteManager().GetSpriteFromId(this.getBackgroundTextureId());
        }

        return this.backgroundSprite;
    }
    
    private Rectangle getBounds()
    {
        if (this.bounds == null)
        {
            Sprite backgroundSprite = this.getBackgroundSprite();
            this.bounds = new Rectangle(0, 0, backgroundSprite.getWidth(), backgroundSprite.getHeight());
        }
        
        return this.bounds;
    }
    
    private HitBox areaHitBox;
    
    public HitBox getAreaHitBox() { return this.areaHitBox; }
    public void setAreaHitBox(HitBox areaHitBox) { this.areaHitBox = areaHitBox; }

    public ArrayList<FieldCutscene> getCutsceneList()
    {
        if (this.cutsceneList == null)
        {
            this.cutsceneList = new ArrayList<FieldCutscene>();

            for (int i = 0; i < this.getCutsceneIdList().size(); i++)
            {
                this.cutsceneList.add(Case.getInstance().getFieldCutsceneManager().GetCutsceneFromId(this.getCutsceneIdList().get(i)));
            }
        }

        return this.cutsceneList;
    }
    
    private ArrayList<String> cutsceneIdList;
    
    public ArrayList<String> getCutsceneIdList() { return this.cutsceneIdList; }
    public void setCutsceneIdList(ArrayList<String> cutsceneIdList) { this.cutsceneIdList = cutsceneIdList; }

    public void AddForegroundElement(ForegroundElement element)
    {
        this.foregroundElementList.add(element);
    }

    public void AddHiddenForegroundElement(HiddenForegroundElement element)
    {
        this.hiddenForegroundElementList.add(element);
    }

    public void AddZoomedView(ZoomedView view)
    {
        this.zoomedViewList.add(view);
    }

    public void AddCharacter(FieldCharacter character)
    {
        this.characterList.add(character);
    }
    
    public void AddCrowd(Crowd crowd)
    {
        this.crowdList.add(crowd);
    }

    public void AddTransition(String targetLocationId, String targetLocationName, HitBox hitBox, TransitionDirection transitionDirection, Vector2 interactionLocation, boolean interactFromAnywhere, String transitionStartSfxId, String transitionEndSfxId, boolean hideWhenLocked, Condition condition, Conversation conversation)
    {
        Transition transition = new Transition(targetLocationId, targetLocationName, hitBox, transitionDirection, interactionLocation, interactFromAnywhere, transitionStartSfxId, transitionEndSfxId, hideWhenLocked, condition, conversation);
        
        this.transitionList.add(transition);
    }
    
    public void AddLoopingSound(String soundId, Vector2 origin)
    {
        this.loopingSoundList.add(new LoopingSound(soundId, origin));
    }

    public void AddStartPosition(String previousLocationId, Vector2 startPositionVector, CharacterDirection startPositionDirection, FieldCharacterDirection startPositionFieldDirection)
    {
        StartPosition startPosition = new StartPosition(startPositionVector, startPositionDirection, startPositionFieldDirection);
        
        if (previousLocationId != null)
        {
            this.previousLocationIdToStartPositionDictionary.put(previousLocationId, startPosition);
        }
        else
        {
            this.startPositionFromMap = startPosition;
        }
    }

    public void AddPartnerStartPosition(String previousLocationId, Vector2 startPositionVector, CharacterDirection startPositionDirection, FieldCharacterDirection startPositionFieldDirection)
    {
        StartPosition startPosition = new StartPosition(startPositionVector, startPositionDirection, startPositionFieldDirection);
        
        if (previousLocationId != null)
        {
            this.previousLocationIdToPartnerStartPositionDictionary.put(previousLocationId, startPosition);
        }
    }
    
    public void AddHeightMap(HeightMap heightMap)
    {
        this.heightMapList.add(heightMap);
    }

    private StartPosition GetStartPositionFromPreviousLocationId(String previousLocationId)
    {
        if (previousLocationId != null)
        {
            return previousLocationIdToStartPositionDictionary.get(previousLocationId);
        }
        else
        {
            return this.startPositionFromMap;
        }
    }

    public void Begin(String previousLocationId)
    {
        if (this.playerCharacter == null)
        {
            this.playerCharacter = Case.getInstance().getFieldCharacterManager().GetPlayerCharacter();
        }

        if (Case.getInstance().getPartnerManager().GetCurrentPartnerId() == null)
        {
            this.partnerCharacter = null;
        }
        else
        {
            this.partnerCharacter = Case.getInstance().getFieldCharacterManager().GetCharacterFromId(Case.getInstance().getPartnerManager().GetCurrentPartnerId());
        }

        if (this.evidenceTab == null)
        {
            this.evidenceTab = new Tab((int)Settings.DisplayBounds.getX() - 2 * Tab.Width - 7, true /* isClickable */, "EVIDENCE", false /* useCancelClickSoundEffect */, TabRow.Bottom);
        }

        if (this.evidenceSelector == null)
        {
            this.evidenceSelector = new EvidenceSelector(true /* isCancelable */, true /* isForCombination */);
        }

        //if (this.partnerTab == null)
        //{
        //    this.partnerTab = new Tab((int)Settings.DisplayBounds.getX() - 2 * Tab.Width - 7, true /* isClickable */, "PARTNER", false /* useCancelClickSoundEffect */, TabRow.Bottom);
        //}

        if (this.partnerInformation == null)
        {
            this.partnerInformation = new PartnerInformation();
        }

        if (this.inEasePartner == null)
        {
            this.inEasePartner = new LinearEase(Settings.DisplayBounds.getX(), 0, AnimationDuration);
        }

        if (this.outEasePartner == null)
        {
            this.outEasePartner = new LinearEase(0, -Settings.DisplayBounds.getX(), AnimationDuration);
        }

        if (this.okTab == null)
        {
            this.okTab = new Tab((int)Settings.DisplayBounds.getX() - Tab.Width - 7, true /* isClickable */, "OK");
        }
        
        if (this.quitTab == null)
        {
            this.quitTab = new Tab(Tab.Width, true /* isClickable */, "QUIT DEMO", false /* useCancelClickSoundEffect */, TabRow.Bottom);
        }

        StartPosition startPosition = this.GetStartPositionFromPreviousLocationId(previousLocationId);
        
        this.playerCharacter.Begin();

        this.playerCharacter.setPosition(startPosition.getPosition());
        this.playerCharacter.setState(FieldCharacterState.Standing);
        this.playerCharacter.setDirection(startPosition.getDirection());

        if (this.partnerCharacter != null)
        {
            this.partnerCharacter.Begin();
            
            this.partnerCharacter.setPosition(this.playerCharacter.getPosition());
            this.partnerCharacter.setState(this.playerCharacter.getState());
            this.partnerCharacter.setDirection(this.playerCharacter.getDirection());
        }

        for (int i = 0; i < this.characterList.size(); i++)
        {
            this.characterList.get(i).Begin();
        }

        for (int i = 0; i < this.foregroundElementList.size(); i++)
        {
            this.foregroundElementList.get(i).Begin();
        }
        
        this.targetInteractiveElement = null;
        this.isTwilightWalking = false;
        this.targetPositionQueue = new LinkedList<Vector2>();
        this.targetPosition = new Vector2(0, 0);
        this.drawingOffsetVector = this.playerCharacter.GetMidPoint().subtract(Settings.DisplayBounds.scalarMultiply(0.5));
        
        // If this would take us off the edge of the map, however,
        // clamp this to the bounds of the background.
        if (this.drawingOffsetVector.getX() < 0)
        {
            this.drawingOffsetVector.setX(0);
        }
        else if (this.drawingOffsetVector.getX() > this.getBackgroundSprite().getWidth() - Settings.DisplayBounds.getX())
        {
            this.drawingOffsetVector.setX(this.getBackgroundSprite().getWidth() - Settings.DisplayBounds.getX());
        }
        
        if (this.drawingOffsetVector.getY() < 0)
        {
            this.drawingOffsetVector.setY(0);
        }
        else if (this.drawingOffsetVector.getY() > this.getBackgroundSprite().getHeight() - Settings.DisplayBounds.getY())
        {
            this.drawingOffsetVector.setY(this.getBackgroundSprite().getHeight() - Settings.DisplayBounds.getY());
        }
    }

    public void Update(GameContainer gc, int delta) throws CaseParseException
    {
        if (this.currentEncounter != null)
        {
            this.currentEncounter.Update(gc, delta);

            if (this.currentEncounter.getIsFinished())
            {
                String currentPartnerId = Case.getInstance().getPartnerManager().GetCurrentPartnerId();

                if (!Utils.equals(this.previousPartnerCharacterId, currentPartnerId))
                {
                    if (currentPartnerId == null)
                    {
                        this.partnerCharacter = null;
                    }
                    else
                    {
                        this.partnerCharacter = Case.getInstance().getFieldCharacterManager().GetCharacterFromId(currentPartnerId);

                        // If this partner character came from this scene, then make its starting position
                        // where the field character used to be.  Otherwise, start it at the player character.
                        FieldCharacter partnerCharacterInField = null;

                        for (int i = 0; i < this.characterList.size(); i++)
                        {
                            FieldCharacter fieldCharacter = this.characterList.get(i);
                            
                            if (Utils.equals(fieldCharacter.getId(), currentPartnerId))
                            {
                                partnerCharacterInField = fieldCharacter;
                                break;
                            }
                        }

                        if (partnerCharacterInField != null)
                        {
                            this.partnerCharacter.setPosition(partnerCharacterInField.getPosition());
                            this.partnerCharacter.setState(partnerCharacterInField.getState());
                            this.partnerCharacter.setDirection(partnerCharacterInField.getDirection());
                            partnerCharacterInField.Reset();
                        }
                        else
                        {
                            this.partnerCharacter.setPosition(this.playerCharacter.getPosition());
                            this.partnerCharacter.setState(this.playerCharacter.getState());
                            this.partnerCharacter.setDirection(this.playerCharacter.getDirection());
                        }

                        this.partnerCharacter.Begin();
                    }
                }

                this.currentEncounter.Reset();
                this.currentEncounter = null;
                MouseHelper.HandleClick();
            }
            else
            {
                return;
            }
        }

        if (this.currentCutscene != null)
        {
            this.currentCutscene.Update(gc, delta);

            if (this.currentCutscene.getIsFinished())
            {
                this.currentCutscene = null;
                MouseHelper.HandleClick();
            }
            else
            {
                return;
            }
        }
        else
        {
            /*for (int i = 0; i < this.getCutsceneList().size(); i++)
            {
                FieldCutscene cutscene = this.getCutsceneList().get(i);
                
                if (cutscene.getIsEnabled() && !cutscene.getHasCompleted())
                {
                    this.currentCutscene = cutscene;
                    this.currentCutscene.Begin();
                    return;
                }
            }*/
        }

        if (this.currentInteractiveForegroundElement != null)
        {
            this.currentInteractiveForegroundElement.UpdateInteraction(gc, delta);

            if (this.currentInteractiveForegroundElement.getIsInteractionFinished())
            {
                this.currentInteractiveForegroundElement = null;
                MouseHelper.HandleClick();
            }
            else
            {
                return;
            }
        }

        if (this.evidenceSelector.getIsShowing())
        {
            this.evidenceSelector.Update(gc, delta);

            if (this.evidenceSelector.getIsShowing())
            {
                return;
            }
            else if (this.evidenceSelector.getEvidenceCombinationConversation() != null)
            {
                Encounter evidenceCombinationEncounter = new Encounter();
                evidenceCombinationEncounter.setOneShotConversation(this.evidenceSelector.getEvidenceCombinationConversation());
                this.currentEncounter = evidenceCombinationEncounter;
                this.currentEncounter.Begin();

                return;
            }
        }

        if (this.inEasePartner.getIsStarted())
        {
            this.okTab.setIsEnabled(false);

            if (!this.inEasePartner.getIsFinished())
            {
                this.inEasePartner.Update(gc, delta);
                this.animationOffsetPartner = (int)this.inEasePartner.GetCurrentValue();
                return;
            }
            else if (this.outEasePartner.getIsStarted() && !this.outEasePartner.getIsFinished())
            {
                this.outEasePartner.Update(gc, delta);
                this.animationOffsetPartner = (int)this.outEasePartner.GetCurrentValue();
                return;
            }
            else if (this.outEasePartner.getIsFinished())
            {
                this.inEasePartner.Reset();
                this.outEasePartner.Reset();
            }
            else
            {
                this.okTab.setIsEnabled(true);
                this.okTab.Update();

                if (this.okTab.getIsClicked())
                {
                    this.outEasePartner.Begin();
                }

                return;
            }
        }

        for (int i = 0; i < this.transitionList.size(); i++)
        {
            Transition transition = this.transitionList.get(i);
            CollisionParameter param = new CollisionParameter();
            
            if (this.playerCharacter.IsCollision(transition.getHitBox(), param))
            {
                this.OnExited(transition.getTargetLocation());
                return;
            }
        }

        this.evidenceTab.setIsEnabled(Case.getInstance().getEvidenceManager().getHasEvidence());
        this.evidenceTab.Update();

        if (this.evidenceTab.getIsClicked())
        {
            this.evidenceSelector.Begin();
            this.evidenceSelector.Show();

            return;
        }

        /*this.partnerTab.setIsEnabled(Case.getInstance().getPartnerManager().GetCurrentPartnerId() != null);
        this.partnerTab.Update();

        if (this.partnerTab.getIsClicked())
        {
            this.partnerInformation.setPartnerId(Case.getInstance().getPartnerManager().GetCurrentPartnerId());
            this.inEasePartner.Begin();
            
            return;
        }*/
        
        this.quitTab.Update();
        
        if (this.quitTab.getIsClicked())
        {
            Case.getInstance().setIsFinished(true);
            return;
        }

        for (int i = 0; i < this.characterList.size(); i++)
        {
            FieldCharacter character = this.characterList.get(i);

            if (this.partnerCharacter == null || !Utils.equals(this.partnerCharacter.getId(), character.getId()))
            {
                character.Update(gc, delta, this.drawingOffsetVector);
                //character.setScale(0.004482f * (character.getPosition().getY() + 30) - 0.7229f);
            }
            
            if (character.getIsClicked())
            {
                this.SetTargetInteractiveElement(character);
            }
        }

        for (int i = 0; i < this.foregroundElementList.size(); i++)
        {
            ForegroundElement foregroundElement = this.foregroundElementList.get(i);
            
            foregroundElement.Update(gc, delta, this.drawingOffsetVector);

            if (foregroundElement.getIsClicked() && foregroundElement.getIsInteractive())
            {
                this.SetTargetInteractiveElement(foregroundElement);
            }
        }

        for (int i = 0; i < this.hiddenForegroundElementList.size(); i++)
        {
            HiddenForegroundElement hiddenForegroundElement = this.hiddenForegroundElementList.get(i);
            
            if (hiddenForegroundElement.getIsDiscovered() ||
                ((this.playerCharacter.GetAnchorPosition().subtract(hiddenForegroundElement.getPosition())).length() < MinDistanceToRevealHiddenElements &&
                (hiddenForegroundElement.getRequiredPartnerId() == null || Utils.equals(hiddenForegroundElement.getRequiredPartnerId(), Case.getInstance().getPartnerManager().GetCurrentPartnerId()))))
            {
                hiddenForegroundElement.setPlayerIsInRange(true);
                hiddenForegroundElement.Update(gc, delta, this.drawingOffsetVector);

                if (hiddenForegroundElement.getIsClicked() && hiddenForegroundElement.getIsInteractive())
                {
                    this.SetTargetInteractiveElement(hiddenForegroundElement);
                }
            }
            else
            {
                hiddenForegroundElement.setPlayerIsInRange(false);
            }
        }
        
        if (MouseHelper.ClickedAnywhere())
        {
            this.targetInteractiveElement = null;
            
            Vector2 clickPoint = MouseHelper.GetMousePosition().add(this.drawingOffsetVector);
            Vector2 endPosition = clickPoint.Clone();
        
            for (int i = 0; i < this.heightMapList.size(); i++)
            {
                HeightMap heightMap = this.heightMapList.get(i);
                Vector2 heightMapOffset = heightMap.GetBasePointOffsetFromHeightenedPoint(endPosition);
                
                if (heightMapOffset != null)
                {
                    endPosition = endPosition.add(heightMapOffset);
                }
            }
        
            this.StartPlayerOnPath(endPosition);
        }
        else if (MouseHelper.PressedAndHeldAnywhere())
        {
            Vector2 endPosition = MouseHelper.GetMousePosition().add(this.drawingOffsetVector).add(new Vector2(0, this.playerCharacter.GetExtraHeight()));
            
            synchronized (this.pathfindingValuesSyncObject)
            {
                this.movingDirectly = true;
                this.targetPositionQueue.clear();
                this.targetPosition = endPosition;
                this.isTwilightWalking = true;
            }
        }
        else if (this.movingDirectly)
        {
            // If moving directly, then we should stop as soon as the player
            // has lifted the mouse button.
            this.isTwilightWalking = false;
        }
        
        synchronized (this.pathfindingValuesSyncObject)
        {
            if (this.isTwilightWalking && this.targetInteractiveElement != null)
            {
                Rectangle boundsForInteraction = this.targetInteractiveElement.GetBoundsForInteraction();
                Vector2 playerAnchorPosition = this.playerCharacter.GetAnchorPosition();

                // If we're close enough to the interactive element,
                // then we should begin the interaction.
                if (((playerAnchorPosition.getX() < boundsForInteraction.getX() && playerAnchorPosition.getX() > boundsForInteraction.getX() - MaxDistanceToStartInteractions) ||
                        (playerAnchorPosition.getX() > boundsForInteraction.getX() && playerAnchorPosition.getX() < boundsForInteraction.getX() + boundsForInteraction.getWidth() + MaxDistanceToStartInteractions)) &&
                    ((playerAnchorPosition.getY() < boundsForInteraction.getY() && playerAnchorPosition.getY() > boundsForInteraction.getY() - MaxDistanceToStartInteractions) ||
                        (playerAnchorPosition.getY() > boundsForInteraction.getY() && playerAnchorPosition.getY() < boundsForInteraction.getY() + boundsForInteraction.getHeight() + MaxDistanceToStartInteractions)))
                {
                    this.targetInteractiveElement.BeginInteraction(this);

                    this.isTwilightWalking = false;
                    this.targetInteractiveElement = null;
                }
            }

            float scaledTwilightSpeed = TwilightSpeed;

            if (this.isTwilightWalking)
            {
                Vector2 currentPlayerPosition = this.playerCharacter.GetAnchorPosition();
                boolean reachedTargetPosition = true;

                if (!this.targetPosition.equals(currentPlayerPosition))
                {
                    Vector2 characterDelta = this.targetPosition.subtract(currentPlayerPosition).normalize().scalarMultiply(scaledTwilightSpeed * (float)delta / 1000);

                    Vector2 newPosition = new Vector2(playerCharacter.getPosition().getX() + characterDelta.getX(), playerCharacter.getPosition().getY() + characterDelta.getY());

                    this.playerCharacter.setPosition(newPosition);
                    this.playerCharacter.setState(FieldCharacterState.Walking);

                    CollisionParameter param = new CollisionParameter();

                    if (this.playerCharacter.IsCollision(this.getAreaHitBox(), param))
                    {
                        param.OverlapAxis = param.OverlapAxis.normalize();
                        newPosition = new Vector2((float)(playerCharacter.getPosition().getX() + param.OverlapAxis.getX() * param.OverlapDistance), (float)(playerCharacter.getPosition().getY() + param.OverlapAxis.getY() * param.OverlapDistance));
                        this.playerCharacter.setPosition(newPosition);
                    }

                    for (int i = 0; i < this.characterList.size(); i++)
                    {
                        FieldCharacter character = this.characterList.get(i);

                        if (this.partnerCharacter == null || !Utils.equals(this.partnerCharacter.getId(), character.getId()))
                        {
                            if (this.playerCharacter.IsCollision(character, param))
                            {
                                param.OverlapAxis = param.OverlapAxis.normalize();
                                newPosition = new Vector2((float)(playerCharacter.getPosition().getX() + param.OverlapAxis.getX() * param.OverlapDistance), (float)(playerCharacter.getPosition().getY() + param.OverlapAxis.getY() * param.OverlapDistance));
                                this.playerCharacter.setPosition(newPosition);
                            }
                        }
                    }
                    
                    Vector2 directionVector = this.targetPosition.subtract(playerCharacter.GetAnchorPosition()).normalize();

                    // If the angle between the two normal vectors is pi / 2 or greater,
                    // then they're not pointing in the same direction, and we've gone past the target position.
                    // We'll back up to the target position and flag that we're done.
                    double angleBetween = Math.acos(Math.min(Math.max(directionVector.dot(characterDelta.normalize()), -1), 1));
                    reachedTargetPosition = angleBetween >= Math.PI / 2;

                    if (!reachedTargetPosition)
                    {
                        // We'll snap the player's direction vector according to
                        // the nearest direction for which we have an animation.
                        double angleToHorizontal = Math.acos(directionVector.getX());
                        
                        // Math.acos() only returns values from 0 to pi,
                        // so to get the full circle we need to check
                        // whether we're in the bottom two quandrants,
                        // and change the angle to account for this if so.
                        if (directionVector.getY() > 0)
                        {
                            angleToHorizontal = 2 * Math.PI - angleToHorizontal;
                        }
                        
                        if (angleToHorizontal <= Math.PI / 8 || angleToHorizontal > Math.PI * 15 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.Side);
                            this.playerCharacter.setDirection(CharacterDirection.Right);
                        }
                        else if (angleToHorizontal > Math.PI / 8 && angleToHorizontal <= Math.PI * 3 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.DiagonalUp);
                            this.playerCharacter.setDirection(CharacterDirection.Right);
                        }
                        else if (angleToHorizontal > 3 * Math.PI / 8 && angleToHorizontal <= Math.PI * 5 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.Up);
                        }
                        else if (angleToHorizontal > 5 * Math.PI / 8 && angleToHorizontal <= Math.PI * 7 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.DiagonalUp);
                            this.playerCharacter.setDirection(CharacterDirection.Left);
                        }
                        else if (angleToHorizontal > 7 * Math.PI / 8 && angleToHorizontal <= Math.PI * 9 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.Side);
                            this.playerCharacter.setDirection(CharacterDirection.Left);
                        }
                        else if (angleToHorizontal > 9 * Math.PI / 8 && angleToHorizontal <= Math.PI * 11 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.DiagonalDown);
                            this.playerCharacter.setDirection(CharacterDirection.Left);
                        }
                        else if (angleToHorizontal > 11 * Math.PI / 8 && angleToHorizontal <= Math.PI * 13 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.Down);
                        }
                        else if (angleToHorizontal > 13 * Math.PI / 8 && angleToHorizontal <= Math.PI * 15 / 8)
                        {
                            this.playerCharacter.setSpriteDirection(FieldCharacterDirection.DiagonalDown);
                            this.playerCharacter.setDirection(CharacterDirection.Right);
                        }
                        
                        if (!this.movingDirectly && this.playerCharacter.GetAnchorPosition().subtract(currentPlayerPosition).length() < 1)
                        {
                            // If the player character moved less than a pixel this frame, she's probably stuck.
                            // If we're moving through pathfinding, we should stop moving as a result.
                            this.playerCharacter.setState(FieldCharacterState.Standing);
                            this.isTwilightWalking = false;
                        }
                    }
                }

                if (reachedTargetPosition)
                {
                    Vector2 nextTargetPosition = this.targetPositionQueue.poll();

                    if (nextTargetPosition == null)
                    {
                        this.playerCharacter.SetPositionByAnchorPosition(this.targetPosition);
                        this.playerCharacter.setState(FieldCharacterState.Standing);
                        this.isTwilightWalking = false;
                    }
                    else
                    {
                        this.targetPosition = nextTargetPosition;
                    }
                }
            }
            else
            {
                this.playerCharacter.setState(FieldCharacterState.Standing);
            }
            
            this.playerCharacter.Update(gc, delta);

            if (this.partnerCharacter != null && (this.isTwilightWalking || this.partnerCharacter.getState() == FieldCharacterState.Walking))
            {
                Vector2 playerCharacterAnchorPoint = this.playerCharacter.GetAnchorPosition();
                Vector2 partnerCharacterAnchorPoint = this.partnerCharacter.GetAnchorPosition();

                Vector2 separationDisplacement = playerCharacterAnchorPoint.subtract(partnerCharacterAnchorPoint);
                int maximumSeparationDisplacement = (int)(this.playerCharacter.getClickRect().getWidth() / 2 + this.partnerCharacter.getClickRect().getWidth() / 2 + 20);

                if (separationDisplacement.length() > maximumSeparationDisplacement)
                {
                    this.partnerCharacter.setState(FieldCharacterState.Walking);

                    float movementDistance = scaledTwilightSpeed * (float)delta / 1000f;

                    if (separationDisplacement.length() - movementDistance < maximumSeparationDisplacement)
                    {
                        movementDistance = (float)(separationDisplacement.length() - maximumSeparationDisplacement);

                        if (movementDistance < 0.001f)
                        {
                            movementDistance = 0.001f;
                        }
                    }

                    Vector2 movementDisplacement = separationDisplacement.normalize().scalarMultiply(movementDistance);
                    this.partnerCharacter.setPosition(this.partnerCharacter.getPosition().add(movementDisplacement));
                }
                else
                {
                    this.partnerCharacter.setState(FieldCharacterState.Standing);
                }

                if (separationDisplacement.getX() < 0)
                {
                    this.partnerCharacter.setDirection(CharacterDirection.Left);
                }
                else if (separationDisplacement.getX() > 0)
                {
                    this.partnerCharacter.setDirection(CharacterDirection.Right);
                }
            }
            
            if (this.partnerCharacter != null)
            {
                this.partnerCharacter.Update(gc, delta);
            }
        }

        for (int i = 0; i < this.characterList.size(); i++)
        {
            FieldCharacter character = this.characterList.get(i);

            if (this.partnerCharacter == null || !Utils.equals(this.partnerCharacter.getId(), character.getId()))
            {
                character.SetExtraHeightFromHeightMaps(this.heightMapList);
            }
        }
        
        this.playerCharacter.SetExtraHeightFromHeightMaps(this.heightMapList);
            
        if (this.partnerCharacter != null)
        {
            this.partnerCharacter.SetExtraHeightFromHeightMaps(this.heightMapList);
        }
        
        // If Twilight's starting to walk off the edge of the screen,
        // move the screen accordingly.
        /*Vector2 playerScreenPosition = this.playerCharacter.GetMidPoint().subtract(this.drawingOffsetVector);
        
        if (playerScreenPosition.getX() < Settings.DisplayBounds.getX() / 3)
        {
            this.drawingOffsetVector.setX(this.drawingOffsetVector.getX() + (playerScreenPosition.getX() - Settings.DisplayBounds.getX() / 3));
        }
        else if (playerScreenPosition.getX() > Settings.DisplayBounds.getX() * 2 / 3)
        {
            this.drawingOffsetVector.setX(this.drawingOffsetVector.getX() + (playerScreenPosition.getX() - Settings.DisplayBounds.getX() * 2 / 3));
        }
        
        if (playerScreenPosition.getY() < Settings.DisplayBounds.getY() / 3)
        {
            this.drawingOffsetVector.setY(this.drawingOffsetVector.getY() + (playerScreenPosition.getY() - Settings.DisplayBounds.getY() / 3));
        }
        else if (playerScreenPosition.getY() > Settings.DisplayBounds.getY() * 2 / 3)
        {
            this.drawingOffsetVector.setY(this.drawingOffsetVector.getY() + (playerScreenPosition.getY() - Settings.DisplayBounds.getY() * 2 / 3));
        }*/
        
        this.drawingOffsetVector = this.playerCharacter.GetMidPoint().subtract(Settings.DisplayBounds.scalarMultiply(0.5));
        
        // If this would take us off the edge of the map, however,
        // clamp this to the bounds of the background.
        if (this.drawingOffsetVector.getX() < 0)
        {
            this.drawingOffsetVector.setX(0);
        }
        else if (this.drawingOffsetVector.getX() > this.getBackgroundSprite().getWidth() - Settings.DisplayBounds.getX())
        {
            this.drawingOffsetVector.setX(this.getBackgroundSprite().getWidth() - Settings.DisplayBounds.getX());
        }
        
        if (this.drawingOffsetVector.getY() < 0)
        {
            this.drawingOffsetVector.setY(0);
        }
        else if (this.drawingOffsetVector.getY() > this.getBackgroundSprite().getHeight() - Settings.DisplayBounds.getY())
        {
            this.drawingOffsetVector.setY(this.getBackgroundSprite().getHeight() - Settings.DisplayBounds.getY());
        }
    }

    public void Draw(GameContainer gc, Graphics g) throws SlickException, CaseParseException
    {
        if (this.currentInteractiveForegroundElement == null || !this.currentInteractiveForegroundElement.getIsInteractionFullScreen())
        {
            this.getBackgroundSprite().DrawClipped(gc, g, new Vector2(0, 0), new Rectangle((float)this.drawingOffsetVector.getX(), (float)this.drawingOffsetVector.getY(), (float)Settings.DisplayBounds.getX(), (float)Settings.DisplayBounds.getY()));

            ArrayList<ZOrderableObject> objectsInZOrder = new ArrayList<ZOrderableObject>();

            for (int i = 0; i < this.foregroundElementList.size(); i++)
            {
                ForegroundElement foregroundElement = this.foregroundElementList.get(i);
                
                if (foregroundElement.IsVisible())
                {
                    objectsInZOrder.add(foregroundElement);
                }
            }

            for (int i = 0; i < this.hiddenForegroundElementList.size(); i++)
            {
                HiddenForegroundElement hiddenForegroundElement = this.hiddenForegroundElementList.get(i);
                
                if (hiddenForegroundElement.getPlayerIsInRange())
                {
                    objectsInZOrder.add(hiddenForegroundElement);
                }
            }

            if (this.currentCutscene != null)
            {
                Collections.sort(objectsInZOrder, new ZOrderableObjectComparator());

                for (int i = 0; i < objectsInZOrder.size(); i++)
                {
                    objectsInZOrder.get(i).Draw(gc, g, this.drawingOffsetVector);
                }
            
                this.currentCutscene.Draw(gc, g, this.drawingOffsetVector);
                return;
            }

            if (this.partnerCharacter != null)
            {
                objectsInZOrder.add(this.partnerCharacter);
            }

            for (int i = 0; i < this.characterList.size(); i++)
            {
                FieldCharacter character = this.characterList.get(i);
                
                if (this.partnerCharacter == null || !Utils.equals(this.partnerCharacter.getId(), character.getId()))
                {
                    objectsInZOrder.add(character);
                }
            }

            objectsInZOrder.add(this.playerCharacter);
            
            Collections.sort(objectsInZOrder, new ZOrderableObjectComparator());

            for (int i = 0; i < objectsInZOrder.size(); i++)
            {
                objectsInZOrder.get(i).Draw(gc, g, this.drawingOffsetVector);
            }
            
            if (Debug.DrawHitBoxes)
            {
                this.areaHitBox.Draw(new Vector2(0, 0).subtract(this.drawingOffsetVector), gc, g);
                
                for (int i = 0; i < objectsInZOrder.size(); i++)
                {
                    FieldCharacter character = Utils.as(objectsInZOrder.get(i), FieldCharacter.class);
                    
                    if (character != null)
                    {
                        character.getHitBox().Draw(character.getPosition().subtract(this.drawingOffsetVector), gc, g);
                    }
                }
            }

            if (this.evidenceSelector.getIsShowing())
            {
                this.evidenceSelector.Draw(gc, g);
            }
            else if (this.inEasePartner.getIsStarted())
            {
                this.partnerInformation.Draw(gc, g, this.animationOffsetPartner);
                this.okTab.Draw(gc, g);
            }
            else
            {
                this.evidenceTab.Draw(gc, g);
                //this.partnerTab.Draw(gc, g);
                this.quitTab.Draw(gc, g);
            }
        }

        if (this.currentInteractiveForegroundElement != null)
        {
            this.currentInteractiveForegroundElement.DrawInteraction(gc, g);
            return;
        }

        if (this.currentEncounter != null)
        {
            this.currentEncounter.Draw(gc, g);
            return;
        }
    }
    
    public void Reset()
    {
        this.currentEncounter = null;
        this.currentCutscene = null;
        this.currentInteractiveForegroundElement = null;
    }
    
    private void SetTargetInteractiveElement(InteractiveElement interactiveElement)
    {
        this.targetInteractiveElement = interactiveElement;
        Vector2 endPosition = interactiveElement.GetCenterPoint();
        
        this.StartPlayerOnPath(endPosition);
    }
    
    private void StartPlayerOnPath(Vector2 endPosition)
    {
        Vector2 currentPlayerPosition = this.playerCharacter.GetAnchorPosition();
        
        synchronized (this.pathfindingValuesSyncObject)
        {
            this.performPathfindingThread = new PerformPathfindingThread(this, currentPlayerPosition, endPosition);
            this.movingDirectly = false;
        }
        
        Thread t = new Thread(this.performPathfindingThread);
        t.setName("MLI Perform Pathfinding Thread");
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();
        
        MouseHelper.HandleClick();
    }
    
    public void BeginCharacterEncounter(FieldCharacter character)
    {
        this.currentEncounter = character.getClickEncounter();

        if (this.currentEncounter != null)
        {
            this.previousPartnerCharacterId = Case.getInstance().getPartnerManager().GetCurrentPartnerId();
            this.currentEncounter.Begin();
        }
    }
    
    public void BeginForegroundElementInteraction(ForegroundElement foregroundElement)
    {
        foregroundElement.BeginInteraction();
        this.currentInteractiveForegroundElement = foregroundElement;
    }

    private void OnExited(Location location)
    {
        LocationEventProvider.RaiseExited(this, location);
    }

    public void SaveToXml(CaseXmlWriter writer) throws IOException, CaseParseException
    {
        writer.StartElement("Location");
        
        Case.getInstance().getSpriteManager().StartGroup("Location" + this.id);
        
        writer.WriteTextElement("Id", this.id);
        writer.WriteSpriteId("BackgroundSpriteId", this.backgroundTextureId);
        writer.WriteTextElement("Bgm", this.bgm);
        
        if (this.ambianceSfxId != null)
        {
            writer.WriteTextElement("AmbianceSfxId", this.ambianceSfxId);
        }
        
        this.areaHitBox.SaveToXml(writer);
        
        writer.StartElement("CutsceneIdList");
        
        for (int i = 0; i < this.cutsceneIdList.size(); i++)
        {
            writer.StartElement("Entry");
            writer.WriteTextElement("CutsceneId", this.cutsceneIdList.get(i));
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("ForegroundElementList");
        
        for (int i = 0; i < this.foregroundElementList.size(); i++)
        {
            writer.StartElement("Entry");
            this.foregroundElementList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("HiddenForegroundElementList");
        
        for (int i = 0; i < this.hiddenForegroundElementList.size(); i++)
        {
            writer.StartElement("Entry");
            this.hiddenForegroundElementList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("ZoomedViewList");
        
        for (int i = 0; i < this.zoomedViewList.size(); i++)
        {
            writer.StartElement("Entry");
            this.zoomedViewList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("CharacterList");
        
        for (int i = 0; i < this.characterList.size(); i++)
        {
            writer.StartElement("Entry");
            this.characterList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("CrowdList");
        
        for (int i = 0; i < this.crowdList.size(); i++)
        {
            writer.StartElement("Entry");
            this.crowdList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("TransitionList");
        
        for (int i = 0; i < this.transitionList.size(); i++)
        {
            writer.StartElement("Entry");
            this.transitionList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("PreviousLocationIdToStartPositionDictionary");
        String[] locationIds = this.previousLocationIdToStartPositionDictionary.keySet().toArray(new String[0]);
        
        for (int i = 0; i < locationIds.length; i++)
        {
            writer.StartElement("Entry");
            
            String locationId = locationIds[i];
            writer.WriteTextElement("LocationId", locationId);
            this.previousLocationIdToStartPositionDictionary.get(locationId).SaveToXml(writer);
            
            writer.EndElement();
        }
        
        writer.EndElement();
        
        if (this.startPositionFromMap != null)
        {
            writer.StartElement("StartPositionFromMap");
            this.startPositionFromMap.SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.StartElement("PreviousLocationIdToPartnerStartPositionDictionary");
        locationIds = this.previousLocationIdToPartnerStartPositionDictionary.keySet().toArray(new String[0]);
        
        for (int i = 0; i < locationIds.length; i++)
        {
            writer.StartElement("Entry");
            
            String locationId = locationIds[i];
            writer.WriteTextElement("LocationId", locationId);
            this.previousLocationIdToPartnerStartPositionDictionary.get(locationId).SaveToXml(writer);
            
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("HeightMapList");
        
        for (int i = 0; i < this.heightMapList.size(); i++)
        {
            writer.StartElement("Entry");
            this.heightMapList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("LoopingSoundList");
        
        for (int i = 0; i < this.loopingSoundList.size(); i++)
        {
            writer.StartElement("Entry");
            this.loopingSoundList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        Case.getInstance().getSpriteManager().EndGroup();
        
        writer.EndElement();
    }

    public Location(CaseXmlReader reader)
    {
        this.foregroundElementList = new ArrayList<ForegroundElement>();
        this.hiddenForegroundElementList = new ArrayList<HiddenForegroundElement>();
        this.zoomedViewList = new ArrayList<ZoomedView>();
        this.characterList = new ArrayList<FieldCharacter>();
        this.transitionList = new ArrayList<Transition>();
        this.previousLocationIdToStartPositionDictionary = new HashMap<String, StartPosition>();
        this.cutsceneIdList = new ArrayList<String>();
        this.heightMapList = new ArrayList<HeightMap>();
        
        reader.StartElement("Location");
        
        this.id = reader.ReadTextElement("Id");
        this.backgroundTextureId = reader.ReadTextElement("BackgroundSpriteId");
        this.areaHitBox = new HitBox(reader);
        
        reader.StartElement("CutsceneIdList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.cutsceneIdList.add(reader.ReadTextElement("CutsceneId"));
        }
        
        reader.EndElement();
        
        reader.StartElement("ForegroundElementList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.foregroundElementList.add(new ForegroundElement(reader));
        }
        
        reader.EndElement();
        
        reader.StartElement("HiddenForegroundElementList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.hiddenForegroundElementList.add(new HiddenForegroundElement(reader));
        }
        
        reader.EndElement();
        
        reader.StartElement("ZoomedViewList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.zoomedViewList.add(new ZoomedView(reader));
        }
        
        reader.EndElement();
        
        reader.StartElement("CharacterList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.characterList.add(new FieldCharacter(reader));
        }
        
        reader.EndElement();
        
        reader.StartElement("TransitionList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.transitionList.add(new Transition(reader));
        }
        
        reader.EndElement();
        
        reader.StartElement("PreviousLocationIdToStartPositionDictionary");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            String locationId = reader.ReadTextElement("LocationId");
            StartPosition startPosition = new StartPosition(reader);
            this.previousLocationIdToStartPositionDictionary.put(locationId, startPosition);
        }
        
        reader.EndElement();
        
        if (reader.ElementExists("StartPositionFromMap"))
        {
            reader.StartElement("StartPositionFromMap");
            this.startPositionFromMap = new StartPosition(reader);
            reader.EndElement();
        }
        
        reader.StartElement("HeightMapList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.heightMapList.add(HeightMap.LoadFromXml(reader));
        }
        
        reader.EndElement();
        
        reader.EndElement();
    }
    
    private class ZOrderableObjectComparator implements Comparator<ZOrderableObject>
    {
        public int compare(ZOrderableObject object1, ZOrderableObject object2)
        {
            return object1.GetZOrder() - object2.GetZOrder();
            //Line line1 = object1.GetZOrderLine();

            /*if (line1 != null)
            {
                Line line2 = object2.GetZOrderLine();
                
                if (line2 != null)
                {
                    // Comparing two lines for z-order is meaningless,
                    // so we'll compare anchor points instead.
                    return (int)(object1.GetZOrderPoint().getY() - object2.GetZOrderPoint().getY());
                }
                else
                {
                    Vector2 minimalDisplacement = line1.GetMinimalDisplacementToPoint(object2.GetZOrderPoint());
                    return (int)minimalDisplacement.getY();
                }
            }
            else
            {
                Line line2 = object2.GetZOrderLine();
                
                if (line2 != null)
                {
                    Vector2 minimalDisplacement = line2.GetMinimalDisplacementToPoint(object1.GetZOrderPoint());
                    return -(int)minimalDisplacement.getY();
                }
                else
                {*/
                    //return (int)(object1.GetZOrderPoint().getY() - object2.GetZOrderPoint().getY());
                //}
           // }
        }
    }
    
    public class Transition
    {
        private String targetLocationId;
        private String targetLocationName;
        private Location targetLocation;
        private HitBox hitBox;
        private TransitionDirection transitionDirection;
        private Vector2 interactionLocation;
        private boolean interactFromAnywhere;
        private String transitionStartSfxId;
        private String transitionEndSfxId;
        private boolean hideWhenLocked;
        private Condition condition;
        private Conversation conversation;

        public Transition(String targetLocationId, String targetLocationName, HitBox hitBox, TransitionDirection transitionDirection, Vector2 interactionLocation, boolean interactFromAnywhere, String transitionStartSfxId, String transitionEndSfxId, boolean hideWhenLocked, Condition condition, Conversation conversation)
        {
            this.targetLocationId = targetLocationId;
            this.targetLocationName = targetLocationName; 
            this.hitBox = hitBox;
            this.transitionDirection = transitionDirection;
            this.interactionLocation = interactionLocation;
            this.interactFromAnywhere = interactFromAnywhere;
            this.transitionStartSfxId = transitionStartSfxId;
            this.transitionEndSfxId = transitionEndSfxId;
            this.hideWhenLocked = hideWhenLocked;
            this.condition = condition;
            this.conversation = conversation; 
        }

        public Location getTargetLocation()
        {
            if (this.targetLocation == null)
            {
                this.targetLocation = Case.getInstance().getContent().GetLocationFromId(this.targetLocationId);
            }

            return this.targetLocation;
        }

        public HitBox getHitBox() { return this.hitBox; }
        public void setHitBox(HitBox hitBox) { this.hitBox = hitBox; }

        private void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("Transition");
            writer.WriteTextElement("TargetLocationId", this.targetLocationId);
            writer.WriteTextElement("TargetLocationName", this.targetLocationName);
            
            writer.StartElement("HitBox");
            this.hitBox.SaveToXml(writer);
            writer.EndElement();
            
            writer.WriteTextElement("TransitionDirection", this.transitionDirection.toString());
            
            writer.StartElement("InteractionLocation");
            this.interactionLocation.SaveToXml(writer);
            writer.EndElement();
            
            writer.WriteBooleanElement("InteractFromAnywhere", this.interactFromAnywhere);
            
            if (this.transitionStartSfxId != null)
            {
                writer.WriteTextElement("TransitionStartSfxId", this.transitionStartSfxId);
            }
            
            if (this.transitionEndSfxId != null)
            {
                writer.WriteTextElement("TransitionEndSfxId", this.transitionEndSfxId);
            }
            
            writer.WriteBooleanElement("HideWhenLocked", this.hideWhenLocked);
            
            if (this.condition != null)
            {
                writer.StartElement("Condition");
                this.condition.SaveToXml(writer);
                writer.EndElement();
            }
            
            if (this.conversation != null)
            {
                writer.StartElement("Conversation");
                this.conversation.SaveToXml(writer);
                writer.EndElement();
            }
            
            writer.EndElement();
        }

        private Transition(CaseXmlReader reader)
        {
            reader.StartElement("Transition");
            this.targetLocationId = reader.ReadTextElement("TargetLocationId");
            this.targetLocationName = reader.ReadTextElement("TargetLocationName");
            
            reader.StartElement("HitBox");
            this.hitBox = new HitBox(reader);
            reader.EndElement();
            
            this.transitionDirection = TransitionDirection.valueOf(reader.ReadTextElement("TransitionDirection"));
            reader.EndElement();
        }
    }

    public class StartPosition
    {
        public StartPosition(Vector2 position, CharacterDirection direction, FieldCharacterDirection fieldDirection)
        {
            this.position = position;
            this.direction = direction;
            this.fieldDirection = fieldDirection; 
        }
        
        private Vector2 position;
        
        public Vector2 getPosition() { return this.position; }
        private void setPosition(Vector2 position) { this.position = position; }
        
        private CharacterDirection direction;
        
        public CharacterDirection getDirection() { return this.direction; }
        private void setDirection(CharacterDirection direction) { this.direction = direction; }
        
        private FieldCharacterDirection fieldDirection;
        
        public FieldCharacterDirection getFieldDirection() { return this.fieldDirection; }
        private void setFieldDirection(FieldCharacterDirection fieldDirection) { this.fieldDirection = fieldDirection; }

        private void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("StartPosition");
            
            writer.StartElement("Position");
            this.position.SaveToXml(writer);
            writer.EndElement();
            
            writer.WriteTextElement("Direction", this.direction.name());
            writer.WriteTextElement("FieldDirection", this.fieldDirection.name());
            writer.EndElement();
        }

        private StartPosition(CaseXmlReader reader)
        {
            reader.StartElement("StartPosition");
            
            reader.StartElement("Position");
            this.position = new Vector2(reader);
            reader.EndElement();
            
            this.direction = CharacterDirection.valueOf(reader.ReadTextElement("Direction"));
            this.fieldDirection = FieldCharacterDirection.valueOf(reader.ReadTextElement("FieldDirection"));
            reader.EndElement();
        }
    }
    
    public class LoopingSound
    {
        private String soundId;
        private Vector2 origin;

        public LoopingSound(String soundId, Vector2 origin)
        {
            this.soundId = soundId;
            this.origin = origin;
        }

        private void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("LoopingSound");
            writer.WriteTextElement("SoundId", this.soundId);
            
            writer.StartElement("Origin");
            this.origin.SaveToXml(writer);
            writer.EndElement();
            
            writer.EndElement();
        }

        private LoopingSound(CaseXmlReader reader)
        {
            reader.StartElement("LoopingSound");
            this.soundId = reader.ReadTextElement("SoundId");
            
            reader.StartElement("Origin");
            this.origin = new Vector2(reader);
            reader.EndElement();
            
            reader.EndElement();
        }
    }
    
    private class PerformPathfindingThread implements Runnable
    {
        private Location location;
        private Vector2 startPosition;
        private Vector2 endPosition;
        
        public PerformPathfindingThread(Location location, Vector2 startPosition, Vector2 endPosition)
        {
            this.location = location;
            this.startPosition = startPosition;
            this.endPosition = endPosition;
        }
        
        public void run()
        {
            Queue<Vector2> targetPositionQueue;
            
            this.endPosition = this.FindClosestPassablePositionForCharacter(this.location.playerCharacter, this.endPosition);
            targetPositionQueue = this.GetPathForCharacterBetweenPoints(this.location.playerCharacter, this.startPosition, this.endPosition);
            targetPositionQueue = this.RemoveUnnecessaryStepsFromPath(this.location.playerCharacter, this.startPosition, targetPositionQueue);
            
            synchronized (this.location.pathfindingValuesSyncObject)
            {
                if (!this.location.movingDirectly && this.location.performPathfindingThread == this)
                {
                    this.location.targetPositionQueue = targetPositionQueue;
                    this.location.targetPosition = this.location.targetPositionQueue.poll();

                    if (this.location.targetPosition != null)
                    {
                        Vector2 displacementDelta = this.location.targetPosition.subtract(this.startPosition);

                        if (displacementDelta.length() > 0.001)
                        {
                            if (displacementDelta.getX() < 0)
                            {
                                this.location.playerCharacter.setDirection(CharacterDirection.Left);
                            }
                            else if (displacementDelta.getX() > 0)
                            {
                                this.location.playerCharacter.setDirection(CharacterDirection.Right);
                            }

                            this.location.isTwilightWalking = true;
                        }
                    }
                    else
                    {
                        this.location.isTwilightWalking = false;
                    }
                }
            }
        }
    
        private Queue<Vector2> RemoveUnnecessaryStepsFromPath(FieldCharacter character, Vector2 startPosition, Queue<Vector2> pathPositionQueue)
        {
            Queue<Vector2> optimizedPath = null;
            boolean optimizationOccurred = true;

            while (pathPositionQueue.size() > 1 && optimizationOccurred)
            {
                Vector2 previousPosition = startPosition;
                Vector2 currentPosition = pathPositionQueue.remove();
                Vector2 nextPosition = pathPositionQueue.element();

                optimizationOccurred = false;
                optimizedPath = new LinkedList<Vector2>();

                while (!pathPositionQueue.isEmpty())
                {
                    // If a collision occurs when we remove the current step,
                    // then this step is needed in the path, so we'll add it
                    // to the optimized path.  Otherwise, we'll leave it out,
                    // and note that an optimization occurred.
                    if (this.IsCollisionBetweenTwoPositions(character, previousPosition, nextPosition))
                    {
                        optimizedPath.add(currentPosition);
                        previousPosition = currentPosition;
                        currentPosition = pathPositionQueue.remove();
                    }
                    else
                    {
                        optimizationOccurred = true;

                        if (pathPositionQueue.size() > 1)
                        {
                            optimizedPath.add(nextPosition);
                            previousPosition = pathPositionQueue.remove();
                            currentPosition = pathPositionQueue.remove();
                        }
                        else
                        {
                            // If we've only got one item left in the queue,
                            // set it to the current position so it'll get
                            // added to the optimized path.
                            currentPosition = pathPositionQueue.remove();
                        }
                    }

                    nextPosition = pathPositionQueue.peek();
                }

                // We always need the last step to be in the path.
                optimizedPath.add(currentPosition);
                pathPositionQueue = optimizedPath;
            }

            return pathPositionQueue;
        }

        private boolean IsCollisionBetweenTwoPositions(FieldCharacter character, Vector2 startPosition, Vector2 endPosition)
        {
            double totalDistance = endPosition.subtract(startPosition).length();
            // TODO: Reconsider this magic number.
            double stepSize = 10;
            Vector2 directionVector = endPosition.subtract(startPosition).normalize().scalarMultiply(stepSize);
            Vector2 currentPosition = startPosition;

            for (double i = 0; i < totalDistance; i += stepSize)
            {
                if (this.TestCollisionWithLocationElements(character, currentPosition))
                {
                    return true;
                }
                else
                {
                    currentPosition = currentPosition.add(directionVector);
                }
            }

            return false;
        }
    
        private Vector2 FindClosestPassablePositionForCharacter(FieldCharacter character, Vector2 position)
        {
            Stack<OverlapEntry> overlapEntries = new Stack<OverlapEntry>();
            Stack<Vector2> positions = new Stack<Vector2>();
            ArrayList<Vector2> possiblePositions = new ArrayList<Vector2>();

            positions.push(position);

            this.FindClosestPassablePositionForCharacter(character, position, overlapEntries, positions, possiblePositions);

            Vector2 closestPosition = character.GetAnchorPosition();
            double closestPositionDistance = Double.POSITIVE_INFINITY;

            for (int i = 0; i < possiblePositions.size(); i++)
            {
                Vector2 possiblePosition = possiblePositions.get(i);
                double possiblePositionDistance = possiblePosition.subtract(position).length();

                if (possiblePositionDistance < closestPositionDistance)
                {
                    closestPositionDistance = possiblePositionDistance;
                    closestPosition = possiblePosition;
                }
            }

            return closestPosition;
        }

        private void FindClosestPassablePositionForCharacter(FieldCharacter character, Vector2 position, Stack<OverlapEntry> overlapEntriesThusFar, Stack<Vector2> positionsThusFar, ArrayList<Vector2> possiblePositions)
        {
            CollisionParameter param = new CollisionParameter();

            if (!this.TestCollisionWithLocationElements(character, position, param) && !possiblePositions.contains(position))
            {
                possiblePositions.add(position);
            }
            else
            {
                for (int i = 0; i < param.OverlapEntryList.size(); i++)
                {
                    OverlapEntry overlapEntry = param.OverlapEntryList.get(i);

                    if (!overlapEntriesThusFar.contains(overlapEntry))
                    {
                        Vector2 newPosition = positionsThusFar.peek().add(overlapEntry.OverlapAxis.scalarMultiply(overlapEntry.OverlapDistance));
                        Rectangle locationBounds = this.location.getBounds();

                        if (newPosition.getX() >= locationBounds.getX() &&
                                newPosition.getX() < locationBounds.getX() + locationBounds.getWidth() &&
                                newPosition.getY() >= locationBounds.getY() &&
                                newPosition.getY() < locationBounds.getY() + locationBounds.getHeight())
                        {
                            positionsThusFar.push(newPosition);
                            overlapEntriesThusFar.push(overlapEntry);

                            this.FindClosestPassablePositionForCharacter(character, newPosition, overlapEntriesThusFar, positionsThusFar, possiblePositions);

                            overlapEntriesThusFar.pop();
                            positionsThusFar.pop();
                        }
                    }
                }
            }
        }

        // Implements the A* search algorithm.
        private Queue<Vector2> GetPathForCharacterBetweenPoints(FieldCharacter character, Vector2 start, Vector2 goal)
        {
            ArrayList<Vector2> closedSet = new ArrayList<Vector2>();
            ArrayList<Vector2> openSet = new ArrayList<Vector2>();
            HashMap<Vector2, Vector2> cameFrom = new HashMap<Vector2, Vector2>();

            // Initialize the open set to have the start position.
            openSet.add(start);

            HashMap<Vector2, Double> gScore = new HashMap<Vector2, Double>();
            HashMap<Vector2, Double> hScore = new HashMap<Vector2, Double>();
            HashMap<Vector2, Double> fScore = new HashMap<Vector2, Double>();

            gScore.put(start, 0.0);
            hScore.put(start, this.HeuristicCostEstimate(start, goal));
            fScore.put(start, gScore.get(start) + hScore.get(start));

            double tileSize = 20;

            double closestPointDistance = Double.POSITIVE_INFINITY;
            Vector2 closestPoint = new Vector2(0, 0);

            while (!openSet.isEmpty())
            {
                Vector2 current = this.GetVectorWithLowestFScore(openSet, fScore);

                if (this.IsPointInTileAtPoint(goal, current, tileSize))
                {
                    closestPoint = current;
                    break;
                }
                else if (goal.subtract(current).length() < closestPointDistance)
                {
                    closestPointDistance = goal.subtract(current).length();
                    closestPoint = current;
                }

                openSet.remove(current);
                closedSet.add(current);

                ArrayList<Vector2> neighborNodes = this.GetNeighbors(character, current, goal, tileSize);

                for (int i = 0; i < neighborNodes.size(); i++)
                {
                    Vector2 neighborNode = neighborNodes.get(i);

                    if (closedSet.contains(neighborNode))
                    {
                        continue;
                    }

                    double tentativeGScore = gScore.get(current) + neighborNode.subtract(current).length();
                    boolean tentativeIsBetter = false;

                    if (!openSet.contains(neighborNode))
                    {
                        openSet.add(neighborNode);
                        hScore.put(neighborNode, this.HeuristicCostEstimate(neighborNode, goal));
                        tentativeIsBetter = true;
                    }
                    else if (tentativeGScore < gScore.get(neighborNode))
                    {
                        tentativeIsBetter = true;
                    }

                    if (tentativeIsBetter)
                    {
                        cameFrom.put(neighborNode, current);
                        gScore.put(neighborNode, tentativeGScore);
                        fScore.put(neighborNode, tentativeGScore + hScore.get(neighborNode));
                    }
                }
            }

            return this.ReconstructPath(cameFrom, closestPoint, goal);
        }

        private double HeuristicCostEstimate(Vector2 start, Vector2 end)
        {
            return end.subtract(start).length();
        }

        private boolean IsPointInTileAtPoint(Vector2 point, Vector2 tilePoint, double tileSize)
        {
            return
                point.getX() - tilePoint.getX() < tileSize / 2 && point.getX() - tilePoint.getX() >= -tileSize / 2 &&
                point.getY() - tilePoint.getY() < tileSize / 2 && point.getY() - tilePoint.getY() >= -tileSize / 2;
        }

        private Vector2 GetVectorWithLowestFScore(ArrayList<Vector2> openSet, HashMap<Vector2, Double> fScore)
        {
            double lowestFScore = Double.POSITIVE_INFINITY;
            Vector2 vectorWithLowestFScore = new Vector2(0, 0);

            for (int i = 0; i < openSet.size(); i++)
            {
                Vector2 currentVector = openSet.get(i);
                double currentFScore = fScore.get(currentVector);

                if (currentFScore < lowestFScore)
                {
                    lowestFScore = currentFScore;
                    vectorWithLowestFScore = currentVector;
                }
            }

            return vectorWithLowestFScore;
        }

        private ArrayList<Vector2> GetNeighbors(FieldCharacter character, Vector2 position, Vector2 goal, double tileSize)
        {
            ArrayList<Vector2> neighborNodes = new ArrayList<Vector2>();

            Vector2 topLeftNeighbor = new Vector2(position.getX() - tileSize, position.getY() - tileSize);

            if (this.IsPointInTileAtPoint(goal, topLeftNeighbor, tileSize))
            {
                topLeftNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, topLeftNeighbor))
            {
                neighborNodes.add(topLeftNeighbor);
            }

            Vector2 topNeighbor = new Vector2(position.getX(), position.getY() - tileSize);

            if (this.IsPointInTileAtPoint(goal, topNeighbor, tileSize))
            {
                topNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, topNeighbor))
            {
                neighborNodes.add(topNeighbor);
            }

            Vector2 topRightNeighbor = new Vector2(position.getX() + tileSize, position.getY() - tileSize);

            if (this.IsPointInTileAtPoint(goal, topRightNeighbor, tileSize))
            {
                topRightNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, topRightNeighbor))
            {
                neighborNodes.add(topRightNeighbor);
            }

            Vector2 leftNeighbor = new Vector2(position.getX() - tileSize, position.getY());

            if (this.IsPointInTileAtPoint(goal, leftNeighbor, tileSize))
            {
                topNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, leftNeighbor))
            {
                neighborNodes.add(leftNeighbor);
            }

            Vector2 rightNeighbor = new Vector2(position.getX() + tileSize, position.getY());

            if (this.IsPointInTileAtPoint(goal, rightNeighbor, tileSize))
            {
                rightNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, rightNeighbor))
            {
                neighborNodes.add(rightNeighbor);
            }

            Vector2 bottomLeftNeighbor = new Vector2(position.getX() - tileSize, position.getY() + tileSize);

            if (this.IsPointInTileAtPoint(goal, bottomLeftNeighbor, tileSize))
            {
                bottomLeftNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, bottomLeftNeighbor))
            {
                neighborNodes.add(bottomLeftNeighbor);
            }

            Vector2 bottomNeighbor = new Vector2(position.getX(), position.getY() + tileSize);

            if (this.IsPointInTileAtPoint(goal, bottomNeighbor, tileSize))
            {
                bottomNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, bottomNeighbor))
            {
                neighborNodes.add(bottomNeighbor);
            }

            Vector2 bottomRightNeighbor = new Vector2(position.getX() + tileSize, position.getY() + tileSize);

            if (this.IsPointInTileAtPoint(goal, bottomRightNeighbor, tileSize))
            {
                bottomRightNeighbor = goal;
            }

            if (!this.TestCollisionWithLocationElements(character, bottomRightNeighbor))
            {
                neighborNodes.add(bottomRightNeighbor);
            }

            return neighborNodes;
        }

        private boolean TestCollisionWithLocationElements(FieldCharacter character, Vector2 position)
        {
            CollisionParameter param = new CollisionParameter();
            position = position.subtract(character.GetAnchorPosition().subtract(character.getPosition()));

            if (character.TestCollisionAtPosition(position, this.location.getAreaHitBox(), param))
            {
                return true;
            }

            if (character != this.location.playerCharacter && character.TestCollisionAtPosition(position, this.location.playerCharacter, param))
            {
                return true;
            }

            for (int i = 0; i < this.location.characterList.size(); i++)
            {
                FieldCharacter characterToTest = this.location.characterList.get(i);

                if (character != characterToTest && (character != this.location.playerCharacter || this.location.partnerCharacter == null || !Utils.equals(this.location.partnerCharacter.getId(), characterToTest.getId())))
                {
                    if (character.TestCollisionAtPosition(position, characterToTest, param))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private boolean TestCollisionWithLocationElements(FieldCharacter character, Vector2 position, CollisionParameter param)
        {
            position = position.subtract(character.GetAnchorPosition().subtract(character.getPosition()));

            if (character.TestCollisionAtPosition(position, this.location.getAreaHitBox(), param))
            {
                return true;
            }

            if (character != this.location.playerCharacter && character.TestCollisionAtPosition(position, this.location.playerCharacter, param))
            {
                return true;
            }

            for (int i = 0; i < this.location.characterList.size(); i++)
            {
                FieldCharacter characterToTest = this.location.characterList.get(i);

                if (character != characterToTest && (character != this.location.playerCharacter || this.location.partnerCharacter == null || !Utils.equals(this.location.partnerCharacter.getId(), characterToTest.getId())))
                {
                    if (character.TestCollisionAtPosition(position, characterToTest, param))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private Queue<Vector2> ReconstructPath(HashMap<Vector2, Vector2> cameFrom, Vector2 currentNode, Vector2 goalNode)
        {
            Queue<Vector2> currentQueue = null;

            if (cameFrom.containsKey(currentNode))
            {
                currentQueue = this.ReconstructPath(cameFrom, cameFrom.get(currentNode));
                currentQueue.add(goalNode);
            }
            else
            {
                currentQueue = new LinkedList<Vector2>();

                // Don't add the current node here - there's no reason to
                // add the start node to the queue, as we're already there.
            }

            return currentQueue;
        }

        private Queue<Vector2> ReconstructPath(HashMap<Vector2, Vector2> cameFrom, Vector2 currentNode)
        {
            Queue<Vector2> currentQueue = null;

            if (cameFrom.containsKey(currentNode))
            {
                currentQueue = this.ReconstructPath(cameFrom, cameFrom.get(currentNode));
                currentQueue.add(currentNode);
            }
            else
            {
                currentQueue = new LinkedList<Vector2>();

                // Don't add the current node here - there's no reason to
                // add the start node to the queue, as we're already there.
            }

            return currentQueue;
        }
    }
    public void markChildSprites() throws CaseParseException
    {
        for (ForegroundElement fe : foregroundElementList)
        {
            fe.markChildSprites(this.id);
        }
        for (HiddenForegroundElement hfe : hiddenForegroundElementList)
        {
            hfe.markChildSprites(this.id);
        }
        
        for (ZoomedView zv : zoomedViewList)
        {
            zv.markChildSprites(this.id);
        }
        
        for (FieldCharacter fc : characterList)
        {
            fc.markChildSprites(this.id, FieldCharacterState.Standing, fc.getSpriteDirection());
            fc.markChildEncounterSprites(this.id);
        }
        
        for (Crowd c : crowdList)
        {
            c.markChildSprites(this.id);
        }
        
        Case.getInstance().getSpriteManager().GetSpriteFromId(this.getBackgroundTextureId()).markChildSprites(this.id);
        
        for (FieldCutscene fc : getCutsceneList())
        {
            for (ForegroundElement fe : foregroundElementList)
            {
                fe.markChildSprites(fc.getId());
            }
        
            Case.getInstance().getSpriteManager().GetSpriteFromId(this.getBackgroundTextureId()).markChildSprites(fc.getId());
            
            fc.markChildSprites();
        }
    }
}